frontend/pages/e/[uuid]/details.tsx (view raw)
1import moment from 'moment';
2import Linkify from 'linkify-react';
3import Tooltip from '@mui/material/Tooltip';
4import IconButton from '@mui/material/IconButton';
5import Box from '@mui/material/Box';
6import Link from '@mui/material/Link';
7import Card from '@mui/material/Card';
8import Container from '@mui/material/Container';
9import TextField from '@mui/material/TextField';
10import Typography from '@mui/material/Typography';
11import TuneIcon from '@mui/icons-material/Tune';
12import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
13import {useTheme} from '@mui/material/styles';
14import {DatePicker} from '@mui/x-date-pickers/DatePicker';
15import {PropsWithChildren, useState} from 'react';
16import {useTranslation} from 'next-i18next';
17import pageUtils from '../../../lib/pageUtils';
18import DetailsLink from '../../../containers/DetailsLink';
19import ShareEvent from '../../../containers/ShareEvent';
20import PlaceInput from '../../../containers/PlaceInput';
21import LangSelector from '../../../components/LangSelector';
22import usePermissions from '../../../hooks/usePermissions';
23import useEventStore from '../../../stores/useEventStore';
24import useToastStore from '../../../stores/useToastStore';
25import EventLayout, {TabComponent} from '../../../layouts/Event';
26import {
27 EventByUuidDocument,
28 useUpdateEventMutation,
29} from '../../../generated/graphql';
30
31interface Props {
32 eventUUID: string;
33 announcement?: string;
34}
35
36const Page = (props: PropsWithChildren<Props>) => {
37 return <EventLayout {...props} Tab={DetailsTab} />;
38};
39
40const DetailsTab: TabComponent<Props> = ({}) => {
41 const {t} = useTranslation();
42 const {
43 userPermissions: {canEditEventDetails},
44 } = usePermissions();
45 const theme = useTheme();
46 const [updateEvent] = useUpdateEventMutation();
47 const addToast = useToastStore(s => s.addToast);
48 const setEventUpdate = useEventStore(s => s.setEventUpdate);
49 const event = useEventStore(s => s.event);
50 const [isEditing, setIsEditing] = useState(false);
51
52 if (!event) return null;
53
54 const onSave = async e => {
55 try {
56 const {uuid, ...data} = event;
57 const {
58 id,
59 travels,
60 waitingPassengers,
61 __typename,
62 administrators,
63 passengers,
64 ...input
65 } = data;
66 await updateEvent({
67 variables: {
68 uuid,
69 eventUpdate: {
70 ...input,
71 },
72 },
73 refetchQueries: ['eventByUUID'],
74 });
75 setIsEditing(false);
76 } catch (error) {
77 console.error(error);
78 addToast(t('event.errors.cant_update'));
79 }
80 };
81
82 const modifyButton = isEditing ? (
83 <Tooltip
84 title={t('event.details.save')}
85 sx={{
86 position: 'absolute',
87 top: theme.spacing(2),
88 right: theme.spacing(2),
89 }}
90 >
91 <IconButton color="primary" onClick={onSave}>
92 <CheckCircleOutlineIcon />
93 </IconButton>
94 </Tooltip>
95 ) : (
96 <Tooltip
97 title={t('event.details.modify')}
98 sx={{
99 position: 'absolute',
100 top: theme.spacing(2),
101 right: theme.spacing(2),
102 }}
103 >
104 <IconButton color="primary" onClick={() => setIsEditing(true)}>
105 <TuneIcon />
106 </IconButton>
107 </Tooltip>
108 );
109
110 return (
111 <Box
112 sx={{
113 position: 'relative',
114 }}
115 >
116 <Container
117 sx={{
118 p: 4,
119 mt: 6,
120 mb: 11,
121 mx: 0,
122 [theme.breakpoints.down('md')]: {
123 p: 2,
124 },
125 }}
126 >
127 <Card
128 sx={{
129 position: 'relative',
130 maxWidth: '100%',
131 width: '480px',
132 p: 2,
133 }}
134 >
135 <Typography variant="h4" pb={2}>
136 {t('event.details')}
137 </Typography>
138 {canEditEventDetails() && modifyButton}
139 {(isEditing || event.name) && (
140 <Box pt={2} pr={1.5}>
141 <Typography variant="overline">
142 {t('event.fields.name')}
143 </Typography>
144 <Typography>
145 {isEditing ? (
146 <TextField
147 size="small"
148 fullWidth
149 value={event.name}
150 onChange={e => setEventUpdate({name: e.target.value})}
151 name="name"
152 id="EditEventName"
153 />
154 ) : (
155 <Typography id="EventName">{event.name}</Typography>
156 )}
157 </Typography>
158 </Box>
159 )}
160 {(isEditing || event.address) && (
161 <Box pt={2} pr={1.5}>
162 <Typography variant="overline">
163 {t('event.fields.date')}
164 </Typography>
165 {isEditing ? (
166 <Typography>
167 <DatePicker
168 slotProps={{
169 textField: {
170 size: 'small',
171 id: `EditEventDate`,
172 fullWidth: true,
173 placeholder: t('event.fields.date_placeholder'),
174 },
175 }}
176 format="DD/MM/YYYY"
177 value={moment(event.date)}
178 onChange={date =>
179 setEventUpdate({
180 date: !date ? null : moment(date).format('YYYY-MM-DD'),
181 })
182 }
183 />
184 </Typography>
185 ) : (
186 <Box position="relative">
187 <Typography id="EventDate">
188 {moment(event.date).format('DD/MM/YYYY')}
189 </Typography>
190 </Box>
191 )}
192 </Box>
193 )}
194 {(isEditing || event.address) && (
195 <Box pt={2} pr={1.5}>
196 <Typography variant="overline">
197 {t('event.fields.address')}
198 </Typography>
199 {isEditing ? (
200 <PlaceInput
201 place={event.address}
202 latitude={event.latitude}
203 longitude={event.longitude}
204 onSelect={({place, latitude, longitude}) =>
205 setEventUpdate({
206 address: place,
207 latitude,
208 longitude,
209 })
210 }
211 />
212 ) : (
213 <Box position="relative">
214 <Typography id="EventAddress" sx={{pr: 3}}>
215 <Link
216 target="_blank"
217 rel="noreferrer"
218 href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
219 event.address
220 )}`}
221 onClick={e => e.preventDefault}
222 >
223 {event.address}
224 </Link>
225 </Typography>
226 </Box>
227 )}
228 </Box>
229 )}
230 {(isEditing || event.description) && (
231 <Box pt={2} pr={1.5}>
232 <Typography variant="overline">
233 {t('event.fields.description')}
234 </Typography>
235 {isEditing ? (
236 <Typography>
237 <TextField
238 fullWidth
239 multiline
240 maxRows={4}
241 inputProps={{maxLength: 250}}
242 value={event.description || ''}
243 onChange={e =>
244 setEventUpdate({description: e.target.value})
245 }
246 id={`EditEventDescription`}
247 name="description"
248 />
249 </Typography>
250 ) : (
251 <Typography
252 id="EventDescription"
253 sx={{pr: 3, whiteSpace: 'pre-line'}}
254 >
255 <Linkify options={{render: DetailsLink}}>
256 {event.description}
257 </Linkify>
258 </Typography>
259 )}
260 </Box>
261 )}
262 {(isEditing || event.lang) && (
263 <Box pt={2} pr={1.5}>
264 <Typography variant="overline">
265 {t('event.fields.lang')}
266 </Typography>
267 {isEditing ? (
268 <LangSelector
269 value={event.lang}
270 onChange={lang => setEventUpdate({lang})}
271 />
272 ) : (
273 <Typography id="EventLang" sx={{pr: 3}}>
274 {t(`PROTECTED.languages.${event.lang}`)}
275 </Typography>
276 )}
277 </Box>
278 )}
279 {!isEditing && (
280 <ShareEvent
281 title={`Caroster ${event.name}`}
282 sx={{width: '100%', mt: 2}}
283 />
284 )}
285 </Card>
286 </Container>
287 </Box>
288 );
289};
290
291export const getServerSideProps = pageUtils.getServerSideProps(
292 async (context, apolloClient) => {
293 const {uuid} = context.query;
294 const {host = ''} = context.req.headers;
295 let event = null;
296
297 // Fetch event
298 try {
299 const {data} = await apolloClient.query({
300 query: EventByUuidDocument,
301 variables: {uuid},
302 });
303 event = data?.eventByUUID?.data;
304 } catch (error) {
305 return {
306 notFound: true,
307 };
308 }
309
310 return {
311 props: {
312 eventUUID: uuid,
313 metas: {
314 title: event?.attributes?.name || '',
315 url: `https://${host}${context.resolvedUrl}`,
316 },
317 },
318 };
319 }
320);
321export default Page;